﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using OpenTK;
using OpenTK.Input;
using OpenTK.Graphics;

#if EMBEDDED
    using OpenTK.Graphics.ES20;
#else
using OpenTK.Graphics.OpenGL;
#endif

namespace MonoSettlers.RenderSystem
{
    /// <summary>
    /// Provides convenient access to GLSL programs. As with other OpenGL related 
    /// unmanaged resources, you MUST dispose any allocated instance explicitly,
    /// otherwise you will get an exception by the time such an leaking object is GCed.
    /// </summary>
    /// <remarks>
    /// Currently shaders are required for object selection, but also to stay conform
    /// to the OpenGL ES 2.0 Specification, which withdraws all fixed-functions as well
    /// as many other convenient APIs. Later, the terrain rendering engine will make
    /// heavy use of shaders.
    /// </remarks>
    internal class GLProgram : IDisposable
    {
        private int? m_ProgramID;

        /// <summary>
        /// Uniform ID of world projection matrix.
        /// </summary>
        public int ProjMatrixLocation { get; private set; }
        /// <summary>
        /// Uniform ID of model matrix.
        /// </summary>
        public int ModelMatrixLocation { get; private set; }
        /// <summary>
        /// Uniform ID of view matrix.
        /// </summary>
        public int ViewMatrixLocation { get; private set; }
        /// <summary>
        /// If used by shader, the uniform ID of stage 1 texture.
        /// </summary>
        public int? Texture1Location { get; private set; }

        /// <summary>
        /// The GLSL program ID. Needed to set custom shader parameters for example.
        /// </summary>
        public int ProgramID { get { return m_ProgramID.Value; } }
        /// <summary>
        /// The vertex shader belonging to this program. Also see <see cref="AutoDisposeShaders"/>.
        /// </summary>
        public GLShader VertexShader { get; private set; }
        /// <summary>
        /// The pixel shader belonging to this program. Also see <see cref="AutoDisposeShaders"/>.
        /// </summary>
        public GLShader PixelShader { get; private set; }
        /// <summary>
        /// If true, then both shaders will automatically be disposed if this GLSL program
        /// is disposed.
        /// </summary>
        public bool AutoDisposeShaders { get; private set; }

        /// <summary>
        /// A resource leak check. Due to wrong thread context, we usually can't release OpenGL resources
        /// in class destructors!
        /// </summary>
        ~GLProgram()
        {
            if (m_ProgramID.HasValue)
                throw new ApplicationException("GLSL program has not been released before GC.");
        }

        /// <summary>
        /// Releases all unmanaged resources associated with this program.
        /// If <see cref="AutoDisposeShaders"/> is NOT set, then you will have to
        /// dispose both shaders yourself.
        /// </summary>
        public void Dispose()
        {
            if (!m_ProgramID.HasValue)
                return;

            if (AutoDisposeShaders)
            {
                if (VertexShader != null)
                {
                    GL.DetachShader(ProgramID, VertexShader.ShaderID); GLRenderer.CheckError();
                    VertexShader.Dispose();
                }

                if (PixelShader != null)
                {
                    GL.DetachShader(ProgramID, PixelShader.ShaderID); GLRenderer.CheckError();
                    PixelShader.Dispose();
                }
            }

            GL.DeleteProgram(ProgramID); GLRenderer.CheckError();

            PixelShader = null;
            VertexShader = null;
            m_ProgramID = null;
        }

        /// <summary>
        /// Creates a new GLSL program from shaders.
        /// </summary>
        /// <param name="inVertexShader">A valid vertex shader.</param>
        /// <param name="inPixelShader">A valid pixel shader.</param>
        /// <param name="inAutoDisposeShaders">True, if both shaders should be disposed when this program is being disposed.</param>
        public GLProgram(GLShader inVertexShader, GLShader inPixelShader, bool inAutoDisposeShaders)
        {
            try
            {
                AutoDisposeShaders = inAutoDisposeShaders;
                VertexShader = inVertexShader;
                PixelShader = inPixelShader;

                if ((inVertexShader.Type != ShaderType.VertexShader) ||
                        (inPixelShader.Type != ShaderType.FragmentShader))
                    throw new ArgumentException();

                // create program (no magic here)
                m_ProgramID = GL.CreateProgram(); GLRenderer.CheckError();

                GL.AttachShader(ProgramID, inVertexShader.ShaderID); GLRenderer.CheckError();
                GL.AttachShader(ProgramID, inPixelShader.ShaderID); GLRenderer.CheckError();
                GL.LinkProgram(ProgramID); GLRenderer.CheckError();
                String log = GL.GetProgramInfoLog(ProgramID);

                if (!log.StartsWith("No errors."))
                    throw new ArgumentException("GLSL program failed to link: \"" + log + "\".");

                int validStatus;

                GL.ValidateProgram(ProgramID);
                GL.GetProgram(ProgramID, ProgramParameter.ValidateStatus, out validStatus);

                if (validStatus != 1)
                    throw new ArgumentException("GLSL program failed to validate.");

                // we require some default uniforms for all shaders (makes no real sense without)
                ProjMatrixLocation = GL.GetUniformLocation(ProgramID, "projMatrix");
                ViewMatrixLocation = GL.GetUniformLocation(ProgramID, "viewMatrix");
                ModelMatrixLocation = GL.GetUniformLocation(ProgramID, "modelMatrix");

                Texture1Location = GL.GetUniformLocation(ProgramID, "tex_Stage1");

                if (ProjMatrixLocation < 0) throw new ArgumentException("GLSL program does not export uniform \"projMatrix\".");
                if (ViewMatrixLocation < 0) throw new ArgumentException("GLSL program does not export uniform \"viewMatrix\".");
                if (ModelMatrixLocation < 0) throw new ArgumentException("GLSL program does not export uniform \"modelMatrix\".");

                // the following are optional
                if (Texture1Location < 0) Texture1Location = null;

            }
            catch(Exception e)
            {
                // If we get an exception in the constructor, there is no way for the caller to explicitly call Dispose()
                Dispose();

                throw e;
            }
        }

        /// <summary>
        /// Binds this program to the pipeline, setting all three matricies.
        /// </summary>
        public void Bind(Matrix4 inProjMatrix, Matrix4 inViewMatrix, Matrix4 inModelMatrix)
        {
            GL.UseProgram(ProgramID);

            // set uniforms
            GL.UniformMatrix4(ProjMatrixLocation, false, ref inProjMatrix);
            GL.UniformMatrix4(ViewMatrixLocation, false, ref inViewMatrix);
            GL.UniformMatrix4(ModelMatrixLocation, false, ref inModelMatrix);
        }

        /// <summary>
        /// Unbinds the program from the pipeline.
        /// </summary>
        public void Unbind()
        {
            GL.UseProgram(0);
        }
    }
}
